README: how this works, assumptions and decisions
A private record of how Tour Watch works and why. Built . Counts marked like this are filled in from this page's data. Current limits and usage appear only in the two counters at the top, and live numbers on coverage are in the "Coverage, gaps and skipped artists" panel.
Planned features
Not built. Nothing runs on a schedule today; every refresh is started by hand.
Scheduled refreshes
- Ticketmaster, weekly. Re-fetch events for every matched artist with
fetch --ignore-zero-flag, which ignores the cached "zero upcoming shows" flag (otherwise a tour announced later is never seen; about 360 of the 650 matched artists carry that flag). Re-look-up unmatched artists about monthly. Ticketmaster stays the primary source because its allowance is large. - JamBase delta, weekly. One area-wide query for events changed since the last clean run, matched locally against every tracked artist. Cost follows how many events changed, not how often it runs. Philadelphia rides along while one search circle reaches it. Measure the real volume first with a one-call run (page one reports the total).
- JamBase top-up, every two weeks. Re-query, in batches of twelve, the artists JamBase has found shows for, plus a rotating slice of the rest (each artist about every eight weeks). This covers a delta's blind spot: shows JamBase already had but dated beyond its six-month window, which only enter the window later. Ticketmaster has no date limit, so far-off tours are covered there.
- Other pending: SeatGeek (adapter built, needs a client ID, never run live); Spotify streaming history for a better ranking; pinning ambiguous Ticketmaster matches (H.E.R., Noname, Brasstracks) by hand.
Safeguards for scheduled runs
- A monthly JamBase allowance target far below the hard stop, with each run's cap computed from what is left and a pace marker on the counter.
- The guards in section 4 stay on. A delta that needs more pages than its limit stops after page one and leaves its checkpoint where it was, so nothing is skipped or half-applied.
- One runner, durable state. The ledger and database live in one place and a fresh cloud session would not have them. That needs a private repository for code and data, and the environment's secret settings for keys. With no ledger, JamBase calls refuse to run until it is re-initialised from JamBase's own count.
- Refused, stopped or over-cap runs are reported, never retried silently. The Spotify export is repeated every few months, not weekly; new artists are classified, looked up once on Ticketmaster and batched twelve to a JamBase request.
Pulling the Spotify library directly
Today the library arrives as Exportify CSVs that are exported and uploaded by hand. Two planned replacements, in the order they would be built. Neither is started.
- Direct from Spotify's API (planned). A one-time login, then a sync command that reads liked songs and saved albums (50 per request) and feeds the same tallies the CSV import does, so ranking, classification and everything downstream are unchanged.
- Login: PKCE authorisation with the single scope
user-library-read. It needs a Client ID from a Spotify developer app, which is not a secret under PKCE. The browser redirect lands on an address that cannot be reached from the cloud container, so the full redirect address is pasted back once to complete the login. - Token storage: the refresh token goes in the private
.envfile only, never in this page or the code. Spotify can issue a new refresh token on every refresh, so each refresh writes the newest one back. A failed refresh stops with a message and is never retried in a loop. - Syncing: artist IDs are stored alongside names, with the existing normalised-name key kept so overrides and skip lists still apply. Liked songs arrive newest first, so routine syncs read only pages newer than the last one. Songs removed from the library are missed that way, so a full re-read runs about monthly or on request.
- Switching over: the API sync first runs next to the CSV import and the two top-1000 lists are compared. Every difference should be explainable (for example a song liked since the export) before the API becomes the default. The CSV path stays as a fallback.
- Conditions to confirm before building: Spotify's 2026 rules for developer apps require the app owner to have active Premium (the app stops working if it lapses) and cap new apps at 5 users. The library read endpoints must still work as documented after the 2026 changes, so the first step is one test request for a single item. The refresh token only survives if the project directory does, the same durable-storage question as scheduled runs. Scheduled runs depend on this option, since a manual export cannot be automated.
- Login: PKCE authorisation with the single scope
- Easier Exportify import (planned, small). Keeps the manual export but removes the friction.
- The import accepts Exportify's "export all" zip directly and unpacks it, instead of needing the individual CSV files.
- This page shows how many days ago the library was last imported, so a stale ranking is obvious.
- Pasting the CSV text into the chat also works as an alternative to attaching files. It stays manual and cannot be scheduled.
1. What this is
A personal tour-date finder. It takes artists from your saved Spotify library, looks up their upcoming shows through official ticketing APIs (nothing is scraped, only your own keys are used) and shows those near New York, Long Island and, optionally, Philadelphia. Spotify's "Live events" tab missed shows for artists you like, so every artist on a ranked list is looked up explicitly. The data lives in a local SQLite database; this page is a snapshot of it, with all filtering done in your browser.
2. The artist list and ranking
- Inputs: Exportify CSVs of Liked Songs and Saved Albums, about 2,100 distinct artists. Playlists, followed artists and streaming history are not included; play counts need Spotify's "Extended streaming history" download, which can take days or weeks.
- Score: 3 × liked songs + 4 × saved albums (once per album per artist). The code also supports playlist-only songs, followed artists and plays, but those inputs are empty. Every artist credited on a track gets full credit, so frequently featured artists rank higher than their own catalogue warrants.
- Position: artists are ranked, excluded artists (section 3) are removed, and the first 1,000 are searched. The "Artists included" slider cuts at that position (not the raw Spotify rank). Hover an artist's name in any view for their position and liked-song and album counts.
3. Who is left out, and why
The aim is to save requests and keep to artists who can play a show. Classification comes from MusicBrainz, which is separate from any ticketing quota, matched by exact normalised name (accents, "The" and "&" folded).
| Category | Rule | Skipped now |
|---|---|---|
| deceased | A person whose MusicBrainz life-span has ended (Duke Ellington, Beethoven, Coltrane) | |
| disbanded | A group whose life-span has ended (Pink Floyd; Panic! At The Disco, 2023) | |
| classical | An active orchestra or choir, or an artist whose top three tags are classical. With no MusicBrainz match, an orchestral-sounding name counts (philharmonic, symphony, orchestra, sinfonie, concerto, chamber choir). Quartets, quintets, trios and big bands are not caught by name, since many are living jazz acts. | |
| unknown | No confident match. Never skipped. | — |
Safeguards against skipping a living artist
- Same-name matches: when several exact matches exist, the winner needs at least 3× the runner-up's tag weight, otherwise the artist is left ambiguous and not skipped.
- Touring evidence wins: a deceased or disbanded artist with upcoming shows on file is never skipped (this kept Bill Evans and D'Angelo).
- Release-year check, for end dates in 1990 or later only: if every saved track came out more than two years after the supposed end, the match is wrong and the artist is kept (this kept Venna). Older artists are exempt because their saved recordings are reissues with modern dates.
- Overrides:
overrides.csv(artist,action,includeorexclude) beats everything. Some living classical soloists are in the classical group on purpose. The full skipped list is in the coverage panel.
4. How shows are found
Ticketmaster (primary)
- One lookup per artist finds their "attraction": an exact normalised-name match among music attractions, so tribute acts and same-name artists are rejected. Exact matches tied on upcoming shows are ambiguous and unused; no exact match is none.
- One events request per matched artist (more only above 200 shows), skipped when Ticketmaster reported zero upcoming shows at lookup time, unless
--ignore-zero-flagis given. That count goes stale either way (Red Hot Chili Peppers reported 1 but had none; an artist reporting 0 may announce a tour later). Past shows are dropped (section 6). Cancelled and vanished shows are kept and flagged, as described next. - Cancelled and vanished shows stay visible, struck out. On every refresh, a show its source marks cancelled is kept with status cancelled, and a show a source stopped listing (a Ticketmaster event gone from the artist's list, or a JamBase deleted-event notice) is kept as no longer listed. Either way the listing stays on this page, crossed out and labelled, so you are not left searching for it; it disappears on its own at 3 AM Eastern after its date, like any past show. If a vanished show comes back it is live again. Rules: any listing saying cancelled marks the show cancelled, even if another site still lists it; a vanished listing hides behind a live duplicate (a re-issued event is not shown twice). A multi-night run with only some nights cancelled stays normal with those nights struck out. A filter, Hide cancelled / no longer listed, removes them, the count line says how many are shown, and the Markdown report and CSV mark them (
statuscolumn). They are left out of the coverage tallies. - Never re-ask: lookups are cached for good, event fetches are skipped within
--fresh-hours, and every run has a hard cap (--max-requests). - It also covers TicketWeb venues (Blue Note, Birdland), so many club shows arrive through it.
JamBase (gap-filler)
- Every searched artist was queried once, in batches: one request carries twelve names (
artistName=A|B|C) with a 110-mile circle around New York and a six-month window (the free plan's limit), 100 events per page. Matching is by keyword, so a show counts only when a listed performer's name matches exactly. A batch needing more than three pages is mostly noise and is abandoned. Results are cached 45 days. Refreshes use a delta: only events changed since the last clean run, matched locally. - Plan: Developer (free). Past its quota JamBase keeps serving and bills $0.05 per call, so nothing server-side stops an overage; your own limit is lower and enforced in code. A "Concert data by JamBase" credit is shown in the footer, as its terms require.
- Links: for a show on both sources the page keeps one link per site (Ticketmaster's direct link, not JamBase's affiliate redirect to it); JamBase-only shows use JamBase's link as returned.
JamBase budget guards
- Hard stop of 500 is a constant in the code and applies to each billing period; settings can only lower it.
- The count is JamBase's billing period, exactly: calls from the period's start to its end, back to zero at the turnover, with no rolling window and nothing carried over. Period dates come from JamBase's quota endpoint; later periods are derived as whole months until the next reading.
- Two layers. (1) A local SQLite ledger plus a separate append-only log; a call is recorded before it is sent, usage is the larger of the two, and concurrent runs cannot both pass. If one file is deleted the other keeps the count; if both go, calls refuse until re-initialised. (2) JamBase's free
/v3/quotaendpoint is read before a run, every 10 calls and afterwards; any failure stops the run. - Per run:
--confirm Nis a hard cap; no automatic retries; two failures in a row stop the run; the generic HTTP helper refuses JamBase URLs, so nothing bypasses the guard. Known HTTP errors are not counted (JamBase does not bill them); timeouts and crashes are. Every response is logged injambase_logfor diagnosis without spending calls.
Considered and not used
- Bandsintown (API for artists and their reps), Songkick (closed to new users), AXS, DICE, Resident Advisor (no public API; scrapers exist but you chose not to scrape). DICE shows still arrive through JamBase. Bandsintown and Songkick accounts linked to Spotify give a no-code alert service alongside this tool.
5. From raw results to listings
- One show, several listings: same artist and date, at the same place (within 1.5 miles or the same venue name), and start times within 30 minutes (or one missing). An 8:00 PM and a 10:30 PM show stay separate, each with its own ticket links.
- Cards: one card is one artist in one city, with nights no more than 7 days apart; Brooklyn in October and again in March is two cards. Two shows on a night are labelled Early and Late, three or more are numbered. Venue spellings that differ between sites are merged within a card (within 0.1 mile, or under a mile with one name containing the other) and the shorter is shown.
- Guest / bill appears when the artist's name is not in the event title (a support slot, or a festival such as Jingle Ball); the title is then shown. New means first seen after the previous fetch run began, so after a big first run most shows are new.
6. Filters, views and counters
- Areas each have a centre, a radius slider (2–100 miles) and an on/off box; a show matches inside any active area, by straight-line distance. NYC + NJ (20 mi) is centred in Manhattan: it takes in Brooklyn, Queens, the Bronx and nearby NJ, but not Asbury Park. Long Island is 40 mi and Philadelphia 25 mi (off by default). Across Long Island Sound, New Haven falls inside the Long Island circle though the drive is longer. Shows over 100 miles from every centre are not stored in the page.
- States can be switched off (Connecticut is off by default). Dates start today, and past shows are never listed: a show stays visible until 3:00 AM Eastern the morning after its date (a 6 PM Tuesday show until 3 AM Wednesday), whatever its start time, using New York time wherever you are viewing from. The same rule applies when the page is built, in the Markdown report and CSV, and when events are fetched, and past events are deleted from the database at each fetch and page build. Artists included hides artists below a position, with a line saying what it hides. Choices are remembered in this browser only.
- Views: Timeline, By artist and a Calendar, all using the same filters, plus the Saved list (below); on a phone, calendar days show a count, and tapping one opens its details.
- Saved list: the ☆ on any listing (Timeline, By artist or the Calendar's day detail) saves it, and the Saved view shows what you saved with a small notes box on each (600 characters, saved as you type). Saved shows ignore the filters, upcoming ones use the latest data (new nights and links appear), and shows that have passed or disappeared from the data stay, with their notes, from the copy kept when you saved them. Removing a show that has a note asks for a second click. Where the page can write to its own storage, your list is private to you (other viewers of the link cannot see it, and neither can I) and survives page updates. Otherwise, for example for a signed-out link visitor, it is kept in this browser only. A show is matched across refreshes by artist, city and overlapping dates.
- Counters are snapshots from the last build, because a published page cannot call either service (and no key is ever in the page). Ticketmaster: requests left today, from its response headers, with the reset time. JamBase: calls left under your limit in the current billing period, with calls used, plan quota, period dates and exact reset time. After a period turns over the page says so until it is rebuilt.
7. Findings and limitations
- What JamBase adds: small-venue shows (DICE, Etix and See Tickets sales), late second shows, festival bills and support slots that Ticketmaster does not list under the artist. The yield falls with rank, and artists Ticketmaster could not find almost never have JamBase shows either. Current numbers are in the coverage panel. A single-artist test and a three-name batch matched Ticketmaster's shows exactly, which verified the query, parsing and batching.
- Not found on Ticketmaster ( of ): mostly small electronic, jazz and indie acts, session players, film composers and YouTubers who rarely tour. A few ambiguous names are real touring acts.
- Saves-only ranking is a weak stand-in for listening: classical and soundtrack albums and featured artists rank too high. Streaming history would fix it.
- No sold-out marker (a decision). Neither source exposes availability: Ticketmaster only reports on sale or off sale, and "off sale" often just means the sale has not opened yet; JamBase has no such field. A marker would be guesswork, so it is left out.
- Cancellations are caught at refresh time only. A show cancelled after the last fetch looks normal until the next refresh, as with any snapshot; the refresh then shows it struck out (section 4). A show can also vanish without a cancellation (a re-issued event, a venue change), and then reads "no longer listed" rather than "cancelled".
- Ticketmaster data is untidy: one show under several IDs, some wrong city fields, times as listed locally. Venue coordinates differ between sources by up to about half a mile.
8. Running it
| Command (project folder) | What it does |
|---|---|
python tourwatch.py ingest data | Reads the Spotify exports and ranks artists. Free. |
python tourwatch.py classify --top 1100 | MusicBrainz classification for the exclusions. |
python tourwatch.py resolve --top 1000 --max-requests N | Ticketmaster lookups for artists not yet resolved. |
python tourwatch.py fetch --top 1000 --fresh-hours 24 --max-requests N | Ticketmaster events for artists not fetched recently. Add --ignore-zero-flag to include artists that had no shows when looked up. |
python tourwatch.py quota | Reads the Ticketmaster allowance (1 request). |
python tourwatch.py jambase plan --include-matched --top 1000 | Offline preview of a JamBase run's cost. |
python tourwatch.py jambase fetch --confirm N --include-matched --top 1000 | Guarded JamBase batches; N is a hard cap on calls. |
python tourwatch.py jambase delta --confirm N | Refresh from changed events only (--max-pages limits its size). |
python tourwatch.py jambase probe --artist "A|B" --confirm 1 | One call for named artists, scored against Ticketmaster. |
python tourwatch.py jambase status / quota | Local usage and period (offline) / JamBase's own count (free call). |
python tourwatch.py report --regions nyc,long-island --condense | A Markdown version of the shows. |
python tourwatch.py page --out tourwatch.html | Rebuilds this page. |
Files: tourwatch.py (commands, report, page), sources.py (Ticketmaster, SeatGeek, Bandsintown adapters), jambase.py (guarded client), classify.py (MusicBrainz), spotify_ingest.py, page_template.html, readme_fragment.html (this text), tests/ (offline), tourwatch.db (everything fetched), jambase_ledger.db and jambase_requests.log (JamBase usage, never delete), .env (keys, never published or committed).
Housekeeping: the API keys were pasted into a chat during setup, so consider rotating the Ticketmaster and JamBase keys. Do not enable JamBase's Claude or ChatGPT connector: its calls use the same monthly counter and bypass the ledger.
9. Build log
- Oct 22, 2026: Ticketmaster searched for the top 100, then 400, then (after MusicBrainz exclusions) the top 1,000 artists, never repeating a request already made.
- Oct 22, 2026: JamBase added with its budget guards, verified with one-artist and three-name test calls against Ticketmaster, then seeded for all 1,000 artists in batches (the unmatched group first, then matched artists by rank), each step capped and reviewed.
- Oct 22, 2026: the JamBase counter and ledger were changed from a conservative rolling window to the exact billing period.
- Oct 22, 2026: README and coverage panel rewritten for the current state.
- Nov 2, 2026: cancelled and vanished shows are now kept and shown struck out instead of being deleted at refresh;
fetch --ignore-zero-flagadded; a saved-shows list with notes added to the page.